/*
 *
 *  * Copyright (c) [2019-2021] [NorthLan](lan6995@gmail.com)
 *  *
 *  * Licensed under the Apache License, Version 2.0 (the "License");
 *  * you may not use this file except in compliance with the License.
 *  * You may obtain a copy of the License at
 *  *
 *  *     http://www.apache.org/licenses/LICENSE-2.0
 *  *
 *  * Unless required by applicable law or agreed to in writing, software
 *  * distributed under the License is distributed on an "AS IS" BASIS,
 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  * See the License for the specific language governing permissions and
 *  * limitations under the License.
 *
 */

package org.lan.iti.iha.oauth2.util;

import cn.hutool.core.codec.Base64;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xkcoding.http.HttpUtil;
import lombok.experimental.UtilityClass;
import org.lan.iti.iha.oauth2.GrantType;
import org.lan.iti.iha.oauth2.OAuth2Config;
import org.lan.iti.iha.oauth2.OAuth2Constants;
import org.lan.iti.iha.oauth2.OAuth2ResponseType;
import org.lan.iti.iha.oauth2.enums.OAuth2EndpointMethodType;
import org.lan.iti.iha.oauth2.pkce.CodeChallengeMethod;
import org.lan.iti.iha.oauth2.security.OAuth2RequestParameter;
import org.lan.iti.iha.security.IhaSecurity;
import org.lan.iti.iha.security.exception.authentication.AuthenticationException;

import java.util.HashMap;
import java.util.Map;

/**
 * OAuth Strategy Util
 *
 * @author NorthLan
 * @date 2021-07-05
 * @url https://noahlan.com
 */
@UtilityClass
public class OAuth2Util {

    /**
     * create code_verifier for pkce mode only.
     * <p>
     * high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
     * from <a href="https://tools.ietf.org/html/rfc3986#section-2.3" target="_blank">Section 2.3 of [RFC3986]</a>, with a minimum length of 43 characters and a maximum length of 128 characters.
     *
     * @return String
     */
    public static String generateCodeVerifier() {
        return Base64.encode(RandomUtil.randomString(50), "UTF-8");
    }

    /**
     * Suitable for OAuth 2.0 pkce enhancement mode.
     *
     * @param codeChallengeMethod s256 / plain
     * @param codeVerifier        Generated by the client
     * @return code challenge
     * @see <a href="https://tools.ietf.org/html/rfc7636#section-4.2" target="_blank">https://tools.ietf.org/html/rfc7636#section-4.2</a>
     */
    public static String generateCodeChallenge(CodeChallengeMethod codeChallengeMethod, String codeVerifier) {
        if (CodeChallengeMethod.S256 == codeChallengeMethod) {
            // https://tools.ietf.org/html/rfc7636#section-4.2
            // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
            return Base64.encodeUrlSafe(SecureUtil.sha256().digest(codeVerifier));
        } else {
            return codeVerifier;
        }
    }

    public static void checkOAuthResponse(Map<String, Object> responseKv, String errorMsg) {
        if (null == responseKv || responseKv.isEmpty()) {
            throw new AuthenticationException(errorMsg);
        }
    }

    public static void checkState(String state, String clientId, boolean verifyState) {
        if (!verifyState) {
            return;
        }
        if (StrUtil.isEmpty(state) || StrUtil.isEmpty(clientId)) {
            throw new AuthenticationException("Illegal state.");

        }
        Object cacheState = IhaSecurity.getContext().getCache().get(OAuth2Constants.STATE_CACHE_KEY.concat(clientId));
        if (null == cacheState || !cacheState.equals(state)) {
            throw new AuthenticationException("Illegal state.");
        }
    }

    /**
     * Check the validity of oauthconfig.
     * <p>
     * 1. For {@code tokenUrl}, this configuration is indispensable for any mode
     * 2. When responsetype = code:
     * - {@code authorizationUrl} and {@code userinfoUrl} cannot be null
     * - {@code clientId} cannot be null
     * - {@code clientSecret} cannot be null when PKCE is not enabled
     * 3. When responsetype = token:
     * - {@code authorizationUrl} and {@code userinfoUrl} cannot be null
     * - {@code clientId} cannot be null
     * - {@code clientSecret} cannot be null
     * 4. When GrantType = password:
     * - {@code username} and {@code password} cannot be null
     *
     * @param oAuth2Config oauth config
     */
    public static void checkOAuthConfig(OAuth2Config oAuth2Config, OAuth2RequestParameter parameter) throws AuthenticationException {
        if (StrUtil.isEmpty(oAuth2Config.getTokenUri())) {
            throw new AuthenticationException("requires a tokenUrl");
        }
        // For authorization code mode and implicit authorization mode
        // refer to: https://tools.ietf.org/html/rfc6749#section-4.1
        // refer to: https://tools.ietf.org/html/rfc6749#section-4.2
        if (StrUtil.equalsAny(oAuth2Config.getResponseType(), OAuth2ResponseType.CODE, OAuth2ResponseType.TOKEN)) {
            if (StrUtil.equals(oAuth2Config.getResponseType(), OAuth2ResponseType.CODE)) {
                if (oAuth2Config.getGrantType() != GrantType.AUTHORIZATION_CODE) {
                    throw new AuthenticationException(
                            String.format("Invalid grantType [%s]. When using authorization code mode, grantType must be `authorization_code`",
                                    oAuth2Config.getGrantType()));
                }
                if (!oAuth2Config.isRequireProofKey() && StrUtil.isEmpty(oAuth2Config.getClientSecret())) {
                    throw new AuthenticationException("requires a clientSecret when PKCE is not enabled.");
                }
            } else {
                if (StrUtil.isEmpty(oAuth2Config.getClientSecret())) {
                    throw new AuthenticationException("requires a clientSecret");
                }
            }
            if (StrUtil.isEmpty(oAuth2Config.getClientId())) {
                throw new AuthenticationException("requires a clientId");
            }
            if (StrUtil.isEmpty(oAuth2Config.getAuthorizationUri())) {
                throw new AuthenticationException("requires a authorizationUrl");
            }
            if (StrUtil.isEmpty(oAuth2Config.getUserInfoUri())) {
                throw new AuthenticationException("requires a userinfoUrl");
            }
        }
        // For password mode
        // refer to: https://tools.ietf.org/html/rfc6749#section-4.3
        else {
            if (oAuth2Config.getGrantType() != GrantType.PASSWORD && oAuth2Config.getGrantType() != GrantType.CLIENT_CREDENTIALS) {
                throw new AuthenticationException("When the response type is none in the oauth2 strategy, a grant type other " +
                        "than the authorization code must be used: " + oAuth2Config.getGrantType());
            }
            if (oAuth2Config.getGrantType() != GrantType.PASSWORD) {
                if (!StrUtil.isAllNotEmpty(parameter.getUsername(), parameter.getPassword())) {
                    throw new AuthenticationException("requires username and password in password certificate grant");
                }
            }
        }
    }

    /**
     * Whether it is the callback request after the authorization of the oauth platform is completed,
     * the judgment basis is as follows:
     * - When {@code response_type} is {@code code}, the {@code code} in the request parameter is empty
     * - When {@code response_type} is {@code token}, the {@code access_token} in the request parameter is empty
     *
     * @param parameter    parameter
     * @param oAuth2Config OAuthConfig
     * @return When true is returned, the current HTTP request is a callback request
     */
    public static boolean isCallback(OAuth2RequestParameter parameter, OAuth2Config oAuth2Config) {
        if (StrUtil.equals(OAuth2ResponseType.CODE, oAuth2Config.getResponseType())) {
            String code = parameter.getCode();
            return !StrUtil.isEmpty(code);
        } else if (StrUtil.equals(oAuth2Config.getResponseType(), OAuth2ResponseType.TOKEN)) {
            String accessToken = parameter.getAccessToken();
            return !StrUtil.isEmpty(accessToken);
        }
        return false;
    }


    /**
     * Different third-party platforms may use different request methods,
     * and some third-party platforms have limited request methods, such as post and get.
     * <p>
     * In the {@code Oauth2Util#request(Oauth2EndpointMethodType, String, Map)},
     * Use the appropriate request method to obtain data by judging the {@code Oauth2EndpointMethodType}
     *
     * @param endpointMethodType Oauth2EndpointMethodType
     * @param url                request Url
     * @param params             Request parameters
     * @return Kv
     */
    public static Map<String, Object> request(OAuth2EndpointMethodType endpointMethodType, String url, Map<String, String> params) {
        String res;
        if (null == endpointMethodType || OAuth2EndpointMethodType.GET == endpointMethodType) {
            res = HttpUtil.get(url, params, false);
        } else {
            res = HttpUtil.post(url, params, false);
        }
        Map<String, Object> result;
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
            result = objectMapper.readValue(res, new TypeReference<Map<String, Object>>() {
            });
        } catch (JsonProcessingException e) {
            result = new HashMap<>();
        }
        return result;
    }
}
